#ifndef __LUA_CLASS_BINDING_H__
#define __LUA_CLASS_BINDING_H__

template <typename CLASS>
struct LuaMethod
{
	typedef int (*MethodPtr)(CLASS*, lua_State*);
	const char *name;
	MethodPtr method, getter, setter;
};

template <typename CLASS>
struct LuaTypeObject
{
	LuaTypeObject(const char *_name, LuaMethod<CLASS> *_methods,
		lua_CFunction _alloc, lua_CFunction _dealloc,
		lua_CFunction _tostring, lua_CFunction _thunk)
		: name(_name), methods(_methods),
		alloc(_alloc), dealloc(_dealloc),
		tostring(_tostring), thunk(_thunk)
	{}
	const char *name;
	LuaMethod<CLASS> *methods;

	lua_CFunction alloc;
	lua_CFunction dealloc;
	lua_CFunction tostring;
	lua_CFunction thunk;

	bool IsValid(lua_State *ls) const
	{
		if (NULL == name || 0 == name[0])
		{
			luaL_error(ls, "invalid object name!");
			return false;
		}
		if (NULL == alloc)
		{
			luaL_error(ls, "invalid alloc function!");
			return false;
		}
		if (NULL == dealloc)
		{
			luaL_error(ls, "invalid dealloc function!");
			return false;
		}
		if (NULL == tostring)
		{
			luaL_error(ls, "invalid to string function!");
			return false;
		}
		if (NULL == methods)
		{
			luaL_error(ls, "must has methods or properties!");
			return false;
		}
		if (NULL == thunk)
		{
			luaL_error(ls, "invalid thunk function!");
			return false;
		}
		return true;
	}
};

template <typename CLASS>
static int LuaClassThunk(lua_State *ls)
{
	typedef int (*MethodPtr)(CLASS*, lua_State*);
	// this version doesn't have object validation
	CLASS *obj = (CLASS*)lua_touserdata(ls, 1);
	lua_remove(ls, 1);
	MethodPtr method = (MethodPtr)lua_touserdata(ls, lua_upvalueindex(1));
	if (method != NULL)
	{
		return method(obj, ls);
	}
	return 0;
}

template <typename CLASS>
static int LuaClassSetter(lua_State *ls)
{
	CLASS *obj = (CLASS*)lua_touserdata(ls, 1);
	if (obj == NULL)
	{
		LogLuaError << "invalid lua object, which has been destroyed!";
		return 0;
	}

	lua_getmetatable(ls, 1);
	lua_pushvalue(ls, 2);
	lua_rawget(ls, -2);
	if(lua_isnil(ls, -1))
	{
		LogLuaError << "invalid member: " << lua_tostring(ls, 2);
		return 0;
	}

	LuaMethod<CLASS>* method = (LuaMethod<CLASS>*)lua_touserdata(ls, -1);
	if (method == NULL)
	{
		LogLuaError << "invalid member: " << lua_tostring(ls, 2);
		return 0;
	}

	lua_pop(ls, 2); // pop method, metatable
	lua_remove(ls, 1); // remove obj
	lua_remove(ls, 1); // remove key

	if (method->setter)
	{
		method->setter(obj, ls);
	}
	return 0;
}

template <typename CLASS>
static int LuaClassIndex(lua_State *ls)
{
	CLASS *obj = (CLASS*)lua_touserdata(ls, 1);
	if (obj == NULL)
	{
		LogLuaError << "invalid lua object, which has been destroyed!";
		return 0;
	}

	lua_getmetatable(ls, 1);
	lua_pushvalue(ls, 2);
	lua_rawget(ls, -2);
	if(lua_isnil(ls, -1))
	{
		LogLuaError << "invalid function name " << lua_tostring(ls, 2);
		return 0;
	}

	LuaMethod<CLASS>* method = (LuaMethod<CLASS>*)lua_touserdata(ls, -1);
	if (method == NULL)
	{
		LogLuaError << "invalid member named" << lua_tostring(ls, 2);
		return 0;
	}

	lua_pop(ls, 2); // pop method, metatable
	lua_remove(ls, 1);
	lua_remove(ls, 1);

	if (method->getter != NULL)
	{
		return method->getter(obj, ls);
	}
	else if (method->method != NULL)
	{
		lua_pushlightuserdata(ls, (void*)method->method);
		lua_pushcclosure(ls, LuaClassThunk<CLASS>, 1);
		return 1;
	}
	else
	{
		LogLuaError << "invalid __index for " << typeid(CLASS).name();
	}
	return 0;
}

template <typename CLASS, typename LuaObject>
class LuaClassBind
{
public:
	typedef int (*MethodPtr)(CLASS*, lua_State*);

	static int Thunk(lua_State *ls)
	{
		LuaObject *lua_obj = (LuaObject*)lua_touserdata(ls, 1);
		if (lua_obj == NULL || lua_obj->c_obj == NULL)
		{
			LogLuaError << "invalid lua object, which has been destroyed!";
			return 0;
		}

		lua_remove(ls, 1);
		MethodPtr method = (MethodPtr)lua_touserdata(ls, lua_upvalueindex(1));
		if (method != NULL)
		{
			return method(lua_obj->c_obj, ls);
		}
		else
		{
			LogLuaError << "Invalid method for " << typeid(LuaObject).name();
		}
		return 0;
	}

	static int Index(lua_State *ls)
	{
		LuaObject *lua_obj = (LuaObject*)lua_touserdata(ls, 1);
		if (lua_obj == NULL || lua_obj->c_obj == NULL)
		{
			LogLuaError << "invalid lua object, which has been destroyed!";
			return 0;
		}

		lua_getmetatable(ls, 1);
		lua_pushvalue(ls, 2);
		lua_rawget(ls, -2);
		if(lua_isnil(ls, -1))
		{
			LogLuaError << "invalid member " << lua_tostring(ls, 2);
			return 0;
		}

		LuaMethod<CLASS>* method = (LuaMethod<CLASS>*)lua_touserdata(ls, -1);
		if (method == NULL)
		{
			LogLuaError << "invalid __index " << lua_tostring(ls, 2) << " for " << typeid(LuaObject).name();
			return 0;
		}

		lua_pop(ls, 2);
		lua_remove(ls, 1);
		lua_remove(ls, 1);

		if (method->getter != NULL)
		{
			return method->getter(lua_obj->c_obj, ls);
		}
		else if (method->method != NULL)
		{
			lua_pushlightuserdata(ls, (void*)method->method);
			lua_pushcclosure(ls, Thunk, 1);
			return 1;
		}
		else
		{
			LogLuaError << "invalid __index for " << typeid(LuaObject).name();
		}
		return 0;
	}

	static int Setter(lua_State *ls)
	{
		LuaObject *lua_obj = (LuaObject*)lua_touserdata(ls, 1);
		if (lua_obj == NULL || lua_obj->c_obj == NULL)
		{
			LogLuaError << "invalid lua object, which has been destroyed!";
			return 0;
		}

		lua_getmetatable(ls, 1);
		lua_pushvalue(ls, 2);
		lua_rawget(ls, -2);
		if(lua_isnil(ls, -1))
		{
			LogLuaError << "invalid member " << lua_tostring(ls, 2);
			return 0;
		}

		LuaMethod<CLASS>* method = (LuaMethod<CLASS>*)lua_touserdata(ls, -1);
		if (method == NULL)
		{
			LogLuaError << "invalid __index " << lua_tostring(ls, 2) << " for " << typeid(LuaObject).name();
			return 0;
		}
		lua_pop(ls, 2);
		lua_remove(ls, 1);
		lua_remove(ls, 1);

		if (method->setter)
		{
			method->setter(lua_obj->c_obj, ls);
		}
		return 0;
	}

	static int ToString(lua_State *ls)
	{
		LuaObject *lua_obj = (LuaObject*)lua_touserdata(ls, 1);
		if (lua_obj == NULL || lua_obj->c_obj == NULL)
		{
			lua_pushfstring(ls, "invalid %s", lua_obj->GetName());
		}
		else
		{
			lua_pushfstring(ls, "%s: %p", lua_obj->GetName(), lua_obj->c_obj);
		}
		return 1;
	}

	static int Dealloc(lua_State *ls)
	{
		LuaObject *lua_obj = (LuaObject*)lua_touserdata(ls, 1);
        lua_obj->~LuaObject();
		return 0;
	}

	static bool Register(lua_State *ls, const char *module, LuaTypeObject<CLASS> &type_object)
	{
		type_object.IsValid(ls);

		luaL_newmetatable(ls, type_object.name);
		int metatable = lua_gettop(ls);

		lua_pushvalue(ls, metatable);
		lua_setmetatable(ls, metatable);

		if (!module || '\0' != module[0])
		{
			luaL_Reg empty_reg = {NULL, NULL};
			luaL_register(ls, module, &empty_reg);
			int module_table = lua_gettop(ls);
			lua_pushstring(ls, type_object.name);
			lua_pushvalue(ls, metatable);
			lua_rawset(ls, module_table);
			lua_pop(ls, 1);
		}
		else
		{
			lua_pushstring(ls, type_object.name);
			lua_pushvalue(ls, metatable);
			lua_rawset(ls, LUA_GLOBALSINDEX);
		}

		lua_pushstring(ls, "__tostring");
		lua_pushcfunction(ls, type_object.tostring);
		lua_rawset(ls, metatable);
		lua_pushstring(ls, "__gc");
		lua_pushcfunction(ls, type_object.dealloc);
		lua_rawset(ls, metatable);
		
		lua_pushstring(ls, "__call");
		lua_pushcfunction(ls, type_object.alloc);
		lua_rawset(ls, metatable);
		
		lua_pushstring(ls, "__index");
		lua_pushcfunction(ls, Index);
		lua_rawset(ls, metatable);

		lua_pushstring(ls, "__newindex");
		lua_pushcfunction(ls, Setter);
		lua_rawset(ls, metatable);
		
		// push all the methods and properties into metatable
		LuaMethod<CLASS> *m = type_object.methods;
		if (NULL != m)
		{
			while (m->name != NULL)
			{
				lua_pushstring(ls, m->name);
				lua_pushlightuserdata(ls, m);
				lua_rawset(ls, metatable);
				++m;
			}
		}

		lua_pop(ls, 1);

		return true;
	}
};

#endif